package org.xx.armory.spring5.wxwork;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.xx.armory.commons.ForLogging;

import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Set;

import static java.util.Arrays.stream;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.Collectors.toUnmodifiableSet;
import static org.apache.commons.lang3.time.DateUtils.addMinutes;
import static org.xx.armory.commons.MapUtils.asStringMap;
import static org.xx.armory.commons.MapUtils.getInteger;
import static org.xx.armory.commons.MapUtils.getListOfMap;
import static org.xx.armory.commons.MapUtils.getString;
import static org.xx.armory.commons.MapUtils.getStringArray;
import static org.xx.armory.commons.Validators.rejectIfBlank;
import static org.xx.armory.commons.Validators.rejectIfNull;

/**
 * 企业微信登录服务。
 */
@Service
public class WxWorkClient
        implements InitializingBean {
    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(WxWorkClient.class);

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 企业ID。每个企业微信对应的企业具有唯一ID。
     */
    @Value("${armory.wx-work.corp-id}")
    private String corpId;

    /**
     * 应用程序使用的密钥。
     */
    @Value("${armory.wx-work.agent-secret}")
    private String agentSecret;

    @Value("${armory.wx-work.get-token-url:https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corp-id}&corpsecret={agent-secret}}")
    private String getTokenUrl;

    @Value("${armory.wx-work.get-user-info-url:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token={access-token}&code={auth-code}}")
    private String getUserInfoUrl;

    @Value("${armory.wx-work.get-department-url:https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token={access-token}&id={department-id}}")
    private String getDepartmentUrl;

    @Value("${armory.wx-work.get-user-url:https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token={access-token}&userid={user-id}}")
    private String getUserUrl;

    @Value("${armory.wx-work.get-tag-url:https://qyapi.weixin.qq.com/cgi-bin/tag/get?access_token={access-token}&tagid={tag-id}}")
    private String getTagUrl;

    private volatile String token;
    private volatile Date tokenExpiryTime;

    public WxWorkClient() {
    }

    @Override
    public void afterPropertiesSet()
            throws Exception {
        this.token = "";
        this.tokenExpiryTime = new Date(0L);
    }

    /**
     * 获取当前可用的凭证
     *
     * @return 当前可用的凭证。
     * @see <a href="https://work.weixin.qq.com/api/doc/90000/90135/91039">获取access-token</a>
     */
    public synchronized String getToken() {
        final var now = new Date();
        if ((this.token) == null || this.token.isEmpty() || now.after(this.tokenExpiryTime)) {
            this.token = getString(doGet(getTokenUrl, Map.of("corp-id", this.corpId, "agent-secret", this.agentSecret)), "access_token");

            // 30分钟后过期。
            this.tokenExpiryTime = addMinutes(now, 30);

            logger.debug("New wx-work token: {}, expired at {}", this.token, this.tokenExpiryTime);
        }

        return this.token;
    }

    /**
     * 使用登录授权码获取当前登录的企业微信帐户。
     *
     * @param authCode
     *         登录授权码。
     * @return 当前登录的企业微信帐户。
     * @see <a href="https://work.weixin.qq.com/api/doc/90000/90135/91437">获取访问用户身份</a>
     */
    public WxWorkUser getUserByAuthCode(
            String authCode
    ) {
        rejectIfBlank(authCode, "authCode");

        final var rsp = doGet(getUserInfoUrl, Map.of("access-token", this.getToken(), "auth-code", authCode));
        final var userId = getString(rsp, "UserId");
        return getUser(userId);
    }

    /**
     * 获取当前登录企业微信用户的标签集合。
     *
     * @param wxWorkUser
     *         企业微信用户。
     * @param expectedTagIds
     *         待检查的标签ID集合。
     * @return 指定的用户具有的标签集合。
     * @see <a href="https://work.weixin.qq.com/api/doc/90000/90135/90213">获取标签成员</a>
     */
    public Set<WxWorkTag> getUserTags(
            WxWorkUser wxWorkUser,
            int... expectedTagIds
    ) {
        rejectIfNull(wxWorkUser, "wxWorkUser");

        return getTags(expectedTagIds)
                .stream()
                .filter(tag -> tag.containsUserId(wxWorkUser.getId()) || tag.containsDepartmentId(wxWorkUser.getDepartmentIds()))
                .collect(toUnmodifiableSet());
    }

    /**
     * 获取指定ID的标签集合。
     *
     * @param tagIds
     *         标签ID。
     * @return 具有指定ID的标签集合。
     */
    public Set<WxWorkTag> getTags(
            int... tagIds
    ) {
        if (tagIds == null || tagIds.length == 0) {
            return emptySet();
        }

        return stream(tagIds)
                .mapToObj(tagId -> {
                    final var rsp = doGet(getTagUrl, Map.of("access-token", this.getToken(), "tag-id", tagId));
                    return new WxWorkTag(
                            tagId,
                            getString(rsp, "tagname"),
                            getListOfMap(rsp, "userlist")
                                    .stream().map(user_ -> getString(user_, "userid")).toArray(String[]::new),
                            getStringArray(rsp, "partylist")
                    );
                })
                .collect(toSet());
    }

    /**
     * 获取部门名称
     *
     * @param departmentId
     *         部门id
     * @return 部门名称
     * @see <a href="https://work.weixin.qq.com/api/doc/90000/90135/90208">获取部门列表</a>
     */
    public String getDepartmentName(
            String departmentId
    ) {
        departmentId = rejectIfBlank(departmentId, "departmentId").strip();

        final var departmentList = getListOfMap(doGet(this.getDepartmentUrl, Map.of("access-token", this.getToken(), "department-id", departmentId)), "department");

        if (departmentList != null && !departmentList.isEmpty()) {
            return getString(departmentList.get(0), "name");
        } else {
            return "";
        }
    }

    /**
     * 根据企业微信用户ID获取企业微信用户。
     *
     * @param userId
     *         企业微信用户ID。
     * @return 当前可用的凭证。
     * @see <a href="https://work.weixin.qq.com/api/doc/90000/90135/90196">读取成员</a>
     */
    public WxWorkUser getUser(
            String userId
    ) {
        userId = rejectIfBlank(userId, "userId").strip();

        final var rsp = doGet(getUserUrl, Map.of("access-token", this.getToken(), "user-id", userId));
        return new WxWorkUser(
                userId,
                getString(rsp, "name"),
                getString(rsp, "alias"),
                getString(rsp, "mobile"),
                getString(rsp, "email"),
                getInteger(rsp, "gender"),
                getStringArray(rsp, "department"),
                getString(rsp, "main_department"),
                getString(rsp, "position"),
                getString(rsp, "avatar"),
                getString(rsp, "address")
        );
    }

    private Map<String, Object> doGet(
            String url,
            Map<String, ?> uriMap
    ) {
        return doExchange(HttpMethod.GET, url, uriMap, null);
    }

    private void logReq(
            HttpMethod httpMethod,
            String url,
            Map<?, ?> pkg
    ) {
        logger.trace("Send data to wx-work ({}){}: {}", httpMethod, url, pkg);
    }

    private void logRsp(
            HttpMethod httpMethod,
            String url,
            Map<?, ?> pkg
    ) {
        logger.trace("Received data from wx-work ({}){}: {}", httpMethod, url, pkg);
    }

    /**
     * 通过HTTP协议发送和接收报文。
     *
     * @param httpMethod
     *         HTTP方法。
     * @param url
     *         目标URL。
     * @param uriMap
     *         用于构造目标URL的参数。
     * @param bodyMap
     *         发送报文的内容。
     * @return 响应报文。
     */
    private Map<String, Object> doExchange(
            HttpMethod httpMethod,
            String url,
            Map<String, ?> uriMap,
            Map<String, ?> bodyMap
    ) {
        final var reqHeaders = new HttpHeaders();
        final var req = new HttpEntity<>(bodyMap, reqHeaders);

        logReq(httpMethod, url, bodyMap);

        final var rsp = this.restTemplate.exchange(url, httpMethod, req, Map.class, uriMap != null ? uriMap : emptyMap());
        final var rspBody = rsp.getBody() != null ? asStringMap(rsp.getBody()) : Collections.<String, Object>emptyMap();

        logRsp(httpMethod, url, rspBody);

        final var errCode = getString(rspBody, "errcode");
        final var errMsg = getString(rspBody, "errmsg");

        if (!"0".equals(errCode)) {
            throw new WxWorkException(errCode, errMsg);
        }

        return rspBody;
    }


}
